Cloudflare Workers間でRPC通信する
Introduction
Cloudflare Workers に Service Binding を利用した RPC 機能が追加されました。
これにより、Workers 間の通信がとても簡単になります。
RPC は、ネットワーク上の 2 つのプログラム間の通信を表現する方法です。
AWS Lambda で別の Lambda を呼び出す場合、invoke を使って呼び出すことができますが、
今回 Workers に追加された機能では、Service Binding に別の Worker を設定することで、
通常の関数を呼び出すのと同じように別の Worker を実行することができます。
今回は、2 つの Workers を作成し、
別Workersを呼び出してみます。
Environment
- MacBook Pro (13-inch, M1, 2020)
- OS : MacOS 14.3.1
- wrangler : 3.48.0
- Node : v20.8.1
Try
では、2 つの Workers を実装していきます。
まずは呼び出される側の Workers を作ります。
% npm create cloudflare@latest
プロジェクト名は hello-rpc、言語は Typescript で作成します。
wranglewr.toml は ↓ のようになってます。
name = "hello-rpc" main = "src/index.ts" compatibility_date = "2024-04-05" compatibility_flags = ["nodejs_compat"]
src/index.ts は下記です。
Calc クラスとそれを生成して返す CalcService を定義しています。
この Workers を直接実行した場合、メッセージを返すだけです。
import { WorkerEntrypoint, RpcTarget } from "cloudflare:workers"; export class Calc extends RpcTarget { #exec_counter = 0; add(a: number, b: number): number { this.#exec_counter += 1; return a + b; } get exec_counter() { return this.#exec_counter; } } export class CalcService extends WorkerEntrypoint { async newCalc() { return new Calc(); } async newFunction() { let value = 0; return (increment = 0) => { value += increment; return value; }; } } export default { fetch() { return new Response("HelloRpc is Healthy!"); }, };
デプロイもしておきます。
% npm run deploy
次は ↑ の Workers を呼び出す側の Workers を作成します。
% npm create cloudflare@latest workers-rpc -- --type=hello-world
wrangler.toml に Service Binding を記述します。
entrypoint は WorkerEntrypoint を継承したクラス(デフォルトの場合は必要なし)を指定します。
services = [ { binding = "CALC_SERVICE", service = "hello-rpc", entrypoint = "CalcService" }, ]
関数を返す Workers を実行してみる
CalcService の newFunction 関数では、increment する関数を返しています。
これを呼び出してみましょう。
workers-rpc/src/index.ts を下記のように実装します。
export interface Env { } export default { async fetch(request, env) { using func = await env.CALC_SERVICE.newFunction(); await func(3); await func(6); let count = await func(10); return new Response(count); } }
env から binding した CACL_SERVICE を使ってそのまま関数を実行しています。
using はこのあたりに説明がありますが、スコープ外に出ると自動でリソース開放(dispose)してくれるヤツです。
Workers RPC の仕様上、呼び出し側(workers-rpc)にオブジェクトが存在する限り
呼び出された側(hello-rpc)のメモリ上に保持されます。
なので、呼び出し側で使い終わったら開放してあげる必要があるので、
適切に管理しましょう。
なお、現時点(2024/04)では、using は V8 でつかえません。
なので、普通に wranglder build とかできません。
そのため、ビルドやデプロイをしたい場合、下記のようにします。
% npx wrangler@using-keyword-experimental build % npx wrangler@using-keyword-experimental deploy
デプロイして発行された URL にアクセスすると、
CalcService から取得した関数を実行できます。
クラスのインスタンスを使う
次は Calc クラスのオブジェクトを取得して使ってみます。
RPC のパラメーターや返り値で定義したクラスを使用するには、
RpcTarget クラスを継承します。
こちらも問題なくオブジェクトが使えます。
export default { async fetch(request, env) { using calc: Calc = await env.CALC_SERVICE.newCalc(); let result = await calc.add(2, 2); console.log("2+2=" + result); let result2 = await calc.add(3, 5); console.log("3+5=" + result2); const count = await calc.exec_counter; return new Response(count); } }
Miniflare でテスト
デプロイして動作させることはできたのですが、
ローカルで動かせないと UnitTest もできないので困ります。
下記のような記述を見つけたので、できるかと思って試してみたのですが、
まだ動作せず。
const mf = new Miniflare({ envPath: true, packagePath: true, wranglerConfigPath: true, workers: [ { name: "a", serviceBindings: { A_RPC_SERVICE: { name: kCurrentWorker, entrypoint: "RpcEntrypoint" }, A_NAMED_SERVICE: { name: "a", entrypoint: "namedEntrypoint" }, B_NAMED_SERVICE: { name: "b", entrypoint: "anotherNamedEntrypoint" }, }, compatibilityFlags: ["rpc"], modules: true, script: ` import { WorkerEntrypoint } from "cloudflare:workers"; export class RpcEntrypoint extends WorkerEntrypoint { ping() { return "a:rpc:pong"; } } export const namedEntrypoint = { fetch(request, env, ctx) { return new Response("a:named:pong"); } }; ... `, }, { name: "b", modules: true, script: ` export const anotherNamedEntrypoint = { fetch(request, env, ctx) { return new Response("b:named:pong"); } }; `, }, ], });
Summary
Binding を使うだけでそのまま(ほぼオーバーヘッドなしで)別 Workers を呼び出せるので、
Workers 間の連携がとても簡単になりました。